.ce 2 Help for Those Really Long Dumb Menu Programs by Robert Ramey I just got my Hewlett Packard Laser Jet II. It seems to have everything. All I have to do is read the manual so I can set up the correct printer control codes. This machine has a zillon options, all with funny codes and rules about which combinations of codes and options are valid. If I want to avoid a lot of tedious work sifting through manual everytime I want to change the printer setup, I'll have to write a program. Since I'm a programmer I don't mind This is an application made to order for a series of menus. Now if you've written one menu driven program you've written them all. Or at least it seems that way. This is another tedious job. Its still better than sifting through the printer manual all the time as its only done once. However, writing a menu driven program is tedious in itself. I decided that this was the last tedious menu program I was going to write. 1. Table Driven Code. My solution is to write once a small piece of code which handles dialog with the operator using a table of menus as a guide. Then for each new menu application all I have to write are the menus themselves and some "action routines" which are invoked when certain points in the menu dialog are touched. Listing 1 shows the menus used for my laser printer program. The labels place each menu in its proper place in the hierarchy. At any time the operator can respond with one of the menu selections, return, or escape. Generally, escape will end the menu sequence. Return will return to the previous menu, and a valid menu selection will display a sub menu. For example, the top menu is the first to be displayed. If the operator selects 2 the menu labeled 2 is displayed. If the operator then selects 1 the menu labeled 21 is displayed. This flow of control will end if a menu selection has no sub menu or a specific "action routine" is specified. In this example, For menu selection "4. Just exit" I have chosen exit(0) as the action routine. When this selection is chosen function exit() will be called with argument 0. If there is no sub menu nor is there any specific "action routine" a function action() will be called with a character pointer argument. This argument contains the sequence of responses that led to the current position in the hierarchy. The basic idea of this program is that the operator moves around the hierarchy of menus at will. Listing 2 shows an example of the laser printer program in action. Listing 3 shows how my laser printer setup program functions with the menus. The main program calls the library function menu() then exits. The program doesn't specify the flow of control. This was specified in the menus. Later I'll show the code for menu() along with how the menus get into the program itself. Now the only thing left is to code the action routines. As the operator makes selections from the menus below selection "2. Alter print setup", function action() is executed. This records his choices in the array p. From time to time he may select option "1. Display current printer setup" to see which combination he has selected so far and to check that is valid. This will invoke the function display(). When he is satisfied, he selects "3. Write printer control codes and exit". The finish() function is then executed which writes the proper codes to the output file. Separating the menus and flow of control from the action routines makes writing a program much more like writing a suite of small almost independent programs. It also makes it much easier to expand the program: All one must do is add on to menus and the action routines. The rest of the program need not be touched. 2. How It Works Each menu is stored in a data structure like that shown in listing 4. To initiate a menu dialog the main program calls menu() with the address of the initial menu as the parameter. Listing 5 shows the menu() function. menu() displays the menu whose address it has recieved as an argument a : character for a prompt and waits for a response. When a response is recieved it is compared against the first character in each line of the menu. If it matches, the corresponding action routine is called with its associated parameter. A hierarchy of menus is implemented by specifying menu() as the action routine with the address of the sub menu as the parameter. When an action routine (including menu()) returns, it should specify which of the previously displayed menus should be repeated. If it returns a value of 0, the immediatly previous menu will be repeated. If it returns a value of 1, the menu two levels back will be repeated, etc. In our laser printer setup program example, the function copies() returns a value of 0 so that the "What do you want to change" menu will be repeated. The function action() returns 1 so that menus such as Choose One. 1. Portrait(vertical) 2. Landscape(horizontal) will not be repeated immediatly after a choice is made. When ever the operator responds to a menu with RETURN, menu() returns a value of 0 which repeats the previous menu. Whenever the operator responds with ESCAPE, menu() returns a value of 99 which will normally return through all the menus back to the initial menu() call. Listing 6 show the C code which implements the menus originally displayed in Listing 1. This should help to clarify the functioning of the menu() function as well as the the laser printer setup program. To summarize, A table driven menu program consists of three separatly compiled modules: .in +4 A set of action routines along with a main program which calls menu(&m) A set of menu data structures containing the text of the menus, action routine addresses and paramters A library function menu() .in -4 On my system, the laser printer setup program is produced by the following commands: cc slptrm cc slptr zlink slptr,slptrm,crunlib/ 3. Polishing Up the System. So far we have simplfied the preparation of menu driven programs by separating the menus from the program code. Now we are now left with the task of coding the original menu into a C data structure. This is almost as bad as coding the menus into the original program. However, the job of translating the menus in listing 1 to the C program module in listing 6 is completely mechanical. It can be turned over to a program we might call a menu compiler. Listing 7 shows the program cmenu.c. This programs reads a file in the format of list 1 and writes a file in the format of listing 6. If the menu in figure 1 is in a file named slptrm.mnu, the new command sequence is: cmenu slptrm.c cc slptrm cc slptr zlink slptr,slptrm,crunlib/ Each menu in the input file to menu compiler should be in the following format: .in +4 The menu index indicating its position in the hierarchy should start in column 1. Subsequent lines should start with tab. The menu question on one or more lines. The menu selections one per line. Menu selections are distinguished by a '.' in the second character position of the text. .in -4 Each menu selection may be followed by a ';' and an action routine and parameter. If no action routine is specified, the menu compiler will fill in a default action routine and parameter. If the menu indices indicate that a sub menu exists, the default action routine is menu() and the default parameter is the address of the submenu. If there exists no sub menu, the default action routine is action() and the default parameter is a pointer to a string which contains the menu responses which brought us to this point. Sometimes I want to insert an action routine within the hierarchy of menus. For example, consider the following menu and code fragment from a modem transfer program: .nf ... 4. send file;gfname(m4) ... 4 Which prototcal do you want to use? 1. XMODEM with check sum 2. XMODEM with crc 3. YMODEM 4. Kermit 5. Simple Xon-Xoff 6. No protocal at all FILE *df; /* disk file pointer */ char filename[MAXFNLENGTH]; /* file name specified by operator */ int gfname(nextmenu) MENU *nextmenu; { fprintf(stderr,"\ntype in file name:"); fgets(filename, MAXFNLENGTH, stdin); if(df = fopen(filename, "r")){ menu(nextmenu); /* continue on with menus */ fclose(df); /* close file on returning from submenu */ } else /* simply display error message */ fprintf(stderr,"\nfile not found"); return 0; } .fi When the operator selects "4. send file" the function gfname is called and a filename is requested. If the fule name is valid the menu dialog continues on to the sub menu 4. Otherwise, an error message is displayed and control returns to the previous menu. This permits me to keep a potentially complicated program in small almost independent fragments. I have to be careful of side effects however. In general, one should undo any side effects on leaving that were set in the action routine. In this case this boils down to closing the opened disk file before returning from gfname(). One final trick which might come in handy is to modify at runtime the action routine addresses or parameters in the menu data structure. This would mean that some menu selections would could alter the position of menus within the hierarchy. Although I havn't done it yet It is interesting to think about. It could allow a program to adapt itself to more experienced users. It smells like self modifying code which is supposed to be no-no. However, I want to experment more in this area before I discard the alternative. 4. Extending the Idea. There a number of ways the basic idea could be altered that might be interesting. First, menu tables could be loaded at run time. instead of compiled into the program. This would permit translation of programs into foreign languages without recompiling or relinking. I decided to compile in the menus for the following reasons: .in +4 it usually saves some disk space since file sizes grow in increments of 2k on my system. having menu data and code in the same file simplifies copying of programs and reduces the number of files. it minimizes size of program as code to read, interpret and load menu data structures does not have to be included. it easier to incorporate action routine addresses and parameters at compile time. .in -4 Another idea would be to extend the menu() function to include help displays any time a help key was pressed. The menu compiler would be extended accordingly. Finally, the menu() function could adjusted for memory mapped systems to permit pop-up menus. None of these changes would alter the menu file nor the action routines of programs currently functioning. 5. A Fly in the Ointment. When I finally got cmenu.c functioning I ran into a suprise. My C compiler, Q/C, will not initialize structures. Hence, the code produced by cmenu.c was not compilable. However, I was determined to eliminate the tedious work of coding menu programs, even though it involved more tedious coding to do so. I wrote a second version of cmenu.c which produces a program module written in Z-80 assembler instead of C. This is listing 7. Its faster and more convenient than the C version, but is not so portable. It can be modified to function with most other assemblers. Now the command sequence to compile and link the laser printer setup program is: cmenu slptrm.z80 zas slptrm cc slptr zlink slptr,slptrm,crunlib/ Now I'm all set to write that next menu driven program. Of course, I can't think of any I need right now. It figures.